﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Globalization;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

namespace Xceed.Silverlight.ListBox.Converters
{
  // Documentation on Path Mini-Language: http://msdn.microsoft.com/en-us/library/ms752293.aspx
  // This source code obtained from Codeplex and the URL is http://stringtopathgeometry.codeplex.com/.
  public class StringToPathGeometryConverter : IValueConverter
  {
    #region Private Methods

    private PathGeometry Parse( string path )
    {
      PathGeometry pathGeometry = null;
      m_formatProvider = CultureInfo.InvariantCulture;
      m_pathString = path;
      m_pathLength = path.Length;
      m_curIndex = 0;

      m_secondLastPoint = new Point( 0, 0 );
      m_lastPoint = new Point( 0, 0 );
      m_lastStart = new Point( 0, 0 );

      m_figureStarted = false;

      bool first = true;

      char last_cmd = ' ';

      while( this.ReadToken() ) // Empty path is allowed in XAML
      {
        char cmd = m_token;

        if( first )
        {
          if( ( cmd != 'M' ) && ( cmd != 'm' ) )  // Path starts with M|m 
          {
            this.ThrowBadToken();
          }

          first = false;
        }

        switch( cmd )
        {
          // Start Point of a Path
          case 'm':
          case 'M':
            // XAML allows multiple points after M/m
            m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

            m_figure = new PathFigure();
            m_figure.StartPoint = m_lastPoint;
            m_figure.IsFilled = StringToPathGeometryConverter.IsFilled;
            m_figure.IsClosed = !StringToPathGeometryConverter.IsClosed;
            //context.BeginFigure(_lastPoint, IsFilled, !IsClosed);
            m_figureStarted = true;
            m_lastStart = m_lastPoint;
            last_cmd = 'M';

            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) )
            {
              m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

              LineSegment lineSegment = new LineSegment();
              lineSegment.Point = m_lastPoint;
              m_figure.Segments.Add( lineSegment );
              last_cmd = 'L';
            }
            break;

          case 'l':
          case 'L':
          case 'h':
          case 'H':
          case 'v':
          case 'V':
            this.EnsureFigure();

            do
            {
              switch( cmd )
              {
                // Line: endPoint
                case 'l':
                case 'L':
                  m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;
                // Horizontal: X
                case 'h':
                case 'H':
                  m_lastPoint.X = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
                  break;
                // Vertical: Y
                case 'v':
                case 'V':
                  m_lastPoint.Y = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
                  break;
              }

              LineSegment lineSegment = new LineSegment();
              lineSegment.Point = m_lastPoint;
              m_figure.Segments.Add( lineSegment );
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            last_cmd = 'L';
            break;

          // Cubic Bezier: controlPoint1 controlPoint2 endPoint
          case 'c':
          case 'C':
          // Smooth cublic Bezier: controlPoint2 endPoint
          case 's':
          case 'S':
            this.EnsureFigure();

            do
            {
              Point point;

              switch( cmd )
              {
                case 's':
                case 'S':
                  if( last_cmd == 'C' )
                  {
                    point = this.Reflect();
                  }
                  else
                  {
                    point = m_lastPoint;
                  }

                  m_secondLastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;

                default:
                  point = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

                  m_secondLastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );
                  break;
              }

              m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );

              BezierSegment bezierSegment = new BezierSegment();
              bezierSegment.Point1 = point;
              bezierSegment.Point2 = m_secondLastPoint;
              bezierSegment.Point3 = m_lastPoint;
              m_figure.Segments.Add( bezierSegment );

              last_cmd = 'C';
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            break;

          // Quadratic Bezier: controlPoint endPoint
          case 'q':
          case 'Q':
          // Smooth Quadratic Bezier: controlPoint endPoint
          case 't':
          case 'T':
            this.EnsureFigure();

            do
            {
              switch( cmd )
              {
                case 't':
                case 'T':
                  if( last_cmd == 'Q' )
                  {
                    m_secondLastPoint = this.Reflect();
                  }
                  else
                  {
                    m_secondLastPoint = m_lastPoint;
                  }

                  m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;

                default:
                  m_secondLastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );
                  break;
              }

              QuadraticBezierSegment quadraticBezierSegment = new QuadraticBezierSegment();
              quadraticBezierSegment.Point1 = m_secondLastPoint;
              quadraticBezierSegment.Point2 = m_lastPoint;
              m_figure.Segments.Add( quadraticBezierSegment );

              last_cmd = 'Q';
            }
            while( this.IsNumber( AllowComma ) );

            break;

          // Elliptical Arc: size rotationAngle isLargeArcFlag sweepDirectionFlag endPoint
          case 'a':
          case 'A':
            this.EnsureFigure();

            do
            {
              // A 3,4 5, 0, 0, 6,7
              double w = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
              double h = this.ReadNumber( AllowComma );
              double rotation = this.ReadNumber( StringToPathGeometryConverter.AllowComma );
              bool large = this.ReadBool();
              bool sweep = this.ReadBool();

              m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );

              ArcSegment arcSegment = new ArcSegment();
              arcSegment.Point = m_lastPoint;
              arcSegment.Size = new Size( w, h );
              arcSegment.RotationAngle = rotation;
              arcSegment.IsLargeArc = large;
              arcSegment.SweepDirection = sweep ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
              m_figure.Segments.Add( arcSegment );
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            last_cmd = 'A';
            break;

          // Close command
          case 'z':
          case 'Z':
            this.EnsureFigure();
            m_figure.IsClosed = StringToPathGeometryConverter.IsClosed;

            m_figureStarted = false;
            last_cmd = 'Z';

            m_lastPoint = m_lastStart; // Set reference point to be first point of current figure
            break;

          default:
            this.ThrowBadToken();
            break;
        }
      }

      if( null != m_figure )
      {
        pathGeometry = new PathGeometry();
        pathGeometry.Figures.Add( m_figure );

      }
      return pathGeometry;
    }

    private void SkipDigits( bool signAllowed )
    {
      // Allow for a sign 
      if( signAllowed && this.More() && ( ( m_pathString[ m_curIndex ] == '-' ) || m_pathString[ m_curIndex ] == '+' ) )
      {
        m_curIndex++;
      }

      while( this.More() && ( m_pathString[ m_curIndex ] >= '0' ) && ( m_pathString[ m_curIndex ] <= '9' ) )
      {
        m_curIndex++;
      }
    }

    private bool ReadBool()
    {
      this.SkipWhiteSpace( StringToPathGeometryConverter.AllowComma );

      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex++ ];

        if( m_token == '0' )
        {
          return false;
        }
        else if( m_token == '1' )
        {
          return true;
        }
      }

      this.ThrowBadToken();

      return false;
    }

    private Point Reflect()
    {
      return new Point( 2 * m_lastPoint.X - m_secondLastPoint.X,
                       2 * m_lastPoint.Y - m_secondLastPoint.Y );
    }

    private void EnsureFigure()
    {
      if( !m_figureStarted )
      {
        m_figure = new PathFigure();
        m_figure.StartPoint = m_lastStart;

        //_context.BeginFigure(_lastStart, IsFilled, !IsClosed);
        m_figureStarted = true;
      }
    }

    private double ReadNumber( bool allowComma )
    {
      if( !this.IsNumber( allowComma ) )
      {
        this.ThrowBadToken();
      }

      bool simple = true;
      int start = m_curIndex;

      //
      // Allow for a sign
      //
      // There are numbers that cannot be preceded with a sign, for instance, -NaN, but it's 
      // fine to ignore that at this point, since the CLR parser will catch this later.
      // 
      if( this.More() && ( ( m_pathString[ m_curIndex ] == '-' ) || m_pathString[ m_curIndex ] == '+' ) )
      {
        m_curIndex++;
      }

      // Check for Infinity (or -Infinity).
      if( this.More() && ( m_pathString[ m_curIndex ] == 'I' ) )
      {
        // 
        // Don't bother reading the characters, as the CLR parser will 
        // do this for us later.
        // 
        m_curIndex = Math.Min( m_curIndex + 8, m_pathLength ); // "Infinity" has 8 characters
        simple = false;
      }
      // Check for NaN 
      else if( this.More() && ( m_pathString[ m_curIndex ] == 'N' ) )
      {
        // 
        // Don't bother reading the characters, as the CLR parser will
        // do this for us later. 
        //
        m_curIndex = Math.Min( m_curIndex + 3, m_pathLength ); // "NaN" has 3 characters
        simple = false;
      }
      else
      {
        SkipDigits( !StringToPathGeometryConverter.AllowSign );

        // Optional period, followed by more digits 
        if( this.More() && ( m_pathString[ m_curIndex ] == '.' ) )
        {
          simple = false;
          m_curIndex++;
          SkipDigits( !StringToPathGeometryConverter.AllowSign );
        }

        // Exponent
        if( this.More() && ( ( m_pathString[ m_curIndex ] == 'E' ) || ( m_pathString[ m_curIndex ] == 'e' ) ) )
        {
          simple = false;
          m_curIndex++;
          SkipDigits( StringToPathGeometryConverter.AllowSign );
        }
      }

      if( simple && ( m_curIndex <= ( start + 8 ) ) ) // 32-bit integer
      {
        int sign = 1;

        if( m_pathString[ start ] == '+' )
        {
          start++;
        }
        else if( m_pathString[ start ] == '-' )
        {
          start++;
          sign = -1;
        }

        int value = 0;

        while( start < m_curIndex )
        {
          value = value * 10 + ( m_pathString[ start ] - '0' );
          start++;
        }

        return value * sign;
      }
      else
      {
        string subString = m_pathString.Substring( start, m_curIndex - start );

        try
        {
          return System.Convert.ToDouble( subString, m_formatProvider );
        }
        catch( FormatException except )
        {
          throw new FormatException( string.Format( "Unexpected character in path '{0}' at position {1}", m_pathString, m_curIndex - 1 ), except );
        }
      }
    }

    private bool IsNumber( bool allowComma )
    {
      bool commaMet = this.SkipWhiteSpace( allowComma );

      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex ];

        // Valid start of a number
        if( ( m_token == '.' ) || ( m_token == '-' ) || ( m_token == '+' ) || ( ( m_token >= '0' ) && ( m_token <= '9' ) )
            || ( m_token == 'I' )  // Infinity
            || ( m_token == 'N' ) ) // NaN 
        {
          return true;
        }
      }

      if( commaMet ) // Only allowed between numbers
      {
        this.ThrowBadToken();
      }

      return false;
    }

    private Point ReadPoint( char cmd, bool allowcomma )
    {
      double x = this.ReadNumber( allowcomma );
      double y = this.ReadNumber( StringToPathGeometryConverter.AllowComma );

      if( cmd >= 'a' ) // 'A' < 'a'. lower case for relative
      {
        x += m_lastPoint.X;
        y += m_lastPoint.Y;
      }

      return new Point( x, y );
    }

    private bool ReadToken()
    {
      this.SkipWhiteSpace( !StringToPathGeometryConverter.AllowComma );

      // Check for end of string 
      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex++ ];

        return true;
      }
      else
      {
        return false;
      }
    }

    private bool More()
    {
      return m_curIndex < m_pathLength;
    }

    // Skip white space, one comma if allowed
    private bool SkipWhiteSpace( bool allowComma )
    {
      bool commaMet = false;

      while( this.More() )
      {
        char ch = m_pathString[ m_curIndex ];

        switch( ch )
        {
          case ' ':
          case '\n':
          case '\r':
          case '\t': // SVG whitespace 
            break;

          case ',':
            if( allowComma )
            {
              commaMet = true;
              allowComma = false; // one comma only
            }
            else
            {
              this.ThrowBadToken();
            }
            break;

          default:
            // Avoid calling IsWhiteSpace for ch in (' ' .. 'z']
            if( ( ( ch > ' ' ) && ( ch <= 'z' ) ) || !Char.IsWhiteSpace( ch ) )
            {
              return commaMet;
            }
            break;
        }

        m_curIndex++;
      }

      return commaMet;
    }

    private void ThrowBadToken()
    {
      throw new FormatException( string.Format( "Unexpected character in path '{0}' at position {1}", m_pathString, m_curIndex - 1 ) );
    }

    private static char GetNumericListSeparator( IFormatProvider provider )
    {
      char numericSeparator = ',';

      // Get the NumberFormatInfo out of the provider, if possible
      // If the IFormatProvider doesn't not contain a NumberFormatInfo, then 
      // this method returns the current culture's NumberFormatInfo. 
      NumberFormatInfo numberFormat = NumberFormatInfo.GetInstance( provider );

      // Is the decimal separator is the same as the list separator?
      // If so, we use the ";". 
      if( ( numberFormat.NumberDecimalSeparator.Length > 0 ) && ( numericSeparator == numberFormat.NumberDecimalSeparator[ 0 ] ) )
      {
        numericSeparator = ';';
      }

      return numericSeparator;
    }

    private string ParseBack( PathGeometry geometry )
    {
      StringBuilder sb = new StringBuilder();
      IFormatProvider provider = new CultureInfo( "en-us" );

      string format = null;

      foreach( PathFigure figure in geometry.Figures )
      {
        sb.Append( "M " + ( ( IFormattable )figure.StartPoint ).ToString( format, provider ) + " " );

        foreach( PathSegment segment in figure.Segments )
        {
          char separator = StringToPathGeometryConverter.GetNumericListSeparator( provider );

          if( segment.GetType() == typeof( LineSegment ) )
          {
            LineSegment lineSegment = segment as LineSegment;

            sb.Append( "L " + ( ( IFormattable )lineSegment.Point ).ToString( format, provider ) + " " );
          }
          else if( segment.GetType() == typeof( BezierSegment ) )
          {
            BezierSegment bezierSegment = segment as BezierSegment;

            sb.Append( String.Format( provider,
                 "C{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "} ",
                 separator,
                 bezierSegment.Point1,
                 bezierSegment.Point2,
                 bezierSegment.Point3
                 ) );
          }
          else if( segment.GetType() == typeof( QuadraticBezierSegment ) )
          {
            QuadraticBezierSegment quadraticBezierSegment = segment as QuadraticBezierSegment;

            sb.Append( String.Format( provider,
                 "Q{1:" + format + "}{0}{2:" + format + "} ",
                 separator,
                 quadraticBezierSegment.Point1,
                 quadraticBezierSegment.Point2 ) );
          }
          else if( segment.GetType() == typeof( ArcSegment ) )
          {
            ArcSegment arcSegment = segment as ArcSegment;

            sb.Append( String.Format( provider,
                 "A{1:" + format + "}{0}{2:" + format + "}{0}{3}{0}{4}{0}{5:" + format + "} ",
                 separator,
                 arcSegment.Size,
                 arcSegment.RotationAngle,
                 arcSegment.IsLargeArc ? "1" : "0",
                 arcSegment.SweepDirection == SweepDirection.Clockwise ? "1" : "0",
                 arcSegment.Point ) );
          }
        }

        if( figure.IsClosed )
          sb.Append( "Z" );
      }

      return sb.ToString();
    }

    #endregion

    #region IValueConverter Methods

    public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
    {
      string path = value as string;

      if( !string.IsNullOrEmpty( path ) )
      {
        return this.Parse( path );
      }
      else
      {
        return null;
      }
    }

    public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
    {
      PathGeometry geometry = value as PathGeometry;

      if( null != geometry )
      {
        return this.ParseBack( geometry );
      }
      else
      {
        return default( string );
      }
    }

    #endregion

    #region Private Fields

    private IFormatProvider m_formatProvider;

    private PathFigure m_figure = null;     // Figure object, which will accept parsed segments
    private string m_pathString;        // Input string to be parsed
    private int m_pathLength;
    private int m_curIndex;          // Location to read next character from
    private bool m_figureStarted;     // StartFigure is effective 

    private Point m_lastStart;         // Last figure starting point
    private Point m_lastPoint;         // Last point 
    private Point m_secondLastPoint;   // The point before last point

    private char m_token;             // Non whitespace character returned by ReadToken 

    #endregion

    #region Private Constants

    private static bool AllowSign = true;
    private static bool AllowComma = true;
    private static bool IsFilled = true;
    private static bool IsClosed = true;

    #endregion
  }
}
